﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Caching;
using System.Web.UI;
using Univar.Helpers;
using System.Web;

namespace Univar
{
	public abstract class StoreBase<T, TStore> : ICloneable
	{
		/// <summary>
		/// Lifetime for the persistent cookie based user ID.
		/// </summary>
		public TimeSpan CookieBasedUserIDLifetime { get; set; }

		protected string _key;

		public Scopes Scope { get; set; }

		public T DefaultValue { get; set; }

		public bool IsAutoClearedOnceRetrieved { get; set; }

		/// <summary>
		/// When true an error is thrown when a serialization, deserialization or decryption error occurs
		/// A null value is returned otherwise.
		/// Default is true.
		/// </summary>
		/// <remarks>One of the reasons an error may be thrown when decrypting a cookie value is that the
		/// latter may have been tampered with.</remarks>
		public bool ThrowExceptionOnDeserializationOrDecryptionError { get; set; }

		public bool _isConnected { get; set; }
		public object _valueBuffer { get; set; }

		//Dictionary<string, T1> IndexedStores;

		public StoreBase() : this(default(T), null, Scopes.Default) { }
		public StoreBase(string key) : this(default(T), key, Scopes.Default) { }
		public StoreBase(string key, Scopes scope) : this(default(T), key, scope) { }
		public StoreBase(T defaultValue, string key) : this(defaultValue, key, Scopes.Default) { }
		public StoreBase(T defaultValue, string key, Scopes scope)
		{
			_key = key;
			_isConnected = true;
			DefaultValue = defaultValue;
			Scope = scope;
			ThrowExceptionOnDeserializationOrDecryptionError = true;
		}

		public string Key
		{
			get
			{
				return StorageUser.GetUserKeyByScope(_key, Scope, _httpContext, CookieBasedUserIDLifetime);
			}
			set
			{
				_key = value;
			}
		}

		HttpContext _httpContext;
		/// <summary>
		/// A reference to the actual HttpContext.
		/// It must be specified when using location bound variables(see Targets and Sources enums) or when
		/// running session or cache operations asynchronously since some delegates(e.g CacheItemRemovedCallback)
		/// do the provide access to the default HttpContext.Current property when handled. In WebForms this 
		/// can be done from within the Form_Load or OnInit methods.
		/// </summary>
		public HttpContext HttpContext
		{
			get { return _httpContext; }
			set { _httpContext = value; }
		}

		public T Value
		{
			get
			{
				var value = _isConnected
							? GetValue(Key)
							: (T)_valueBuffer;

				if (IsAutoClearedOnceRetrieved)
					Clear();

				return value;
			}
			set
			{
				if (_isConnected)
					SetValue(Key, value);
				else
					_valueBuffer = value;
			}
		}


		/// <summary>
		/// Gets the value of type T for the store at the specified key.
		/// </summary>
		/// <param name="key">The key used to access the value.</param>
		protected abstract T GetValue(string key);

		/// <summary>
		/// Sets the value of type T in the store at the specified key.
		/// </summary>
		/// <param name="key">The target key.</param>
		/// <param name="value">The value to be stored.</param>
		protected abstract void SetValue(string key, T value);

		/// <summary>
		/// Gets or sets the unboxed and unprocessed value of the variable at the predefined key
		/// in its crude and original state. Care must be taken when using the setter since this
		/// can potentially corrupt the data.
		/// </summary>
		protected abstract object PlainValue { get; set; }



		public bool KeyExists
		{
			get { return PlainValue != null; }
		}

		public virtual void Clear()
		{
			if (_isConnected)
				PlainValue = null;
			else
				_valueBuffer = null;
		}


		public TStore this[Enum index]
		{
			get { return this[index.ToString()]; }
		}

		public TStore this[double index]
		{
			get { return this[index.ToString()]; }
		}

		/// <summary>
		/// Returns a clone of the actual store having a key derived from the concatenation of the 
		/// original store key and the specified index value.
		/// </summary>
		/// <param name="index">The child key to be appended to the parent key using 
		/// Global.KeyDelimiter as seperator.</param>
		/// <returns></returns>
		public virtual TStore this[string index]
		{
			get
			{
				dynamic clone = this.ShallowClone();
				clone.Key += Global.KeyDelimiter + index;
				return clone;
			}
		}

		public T GetChildValue(Enum childKey)
		{
			return GetChildValue(childKey.ToString());
		}

		public void SetChildValue(T value, Enum childKey)
		{
			SetChildValue(childKey.ToString(), value);
		}

		public T GetChildValue(double childKey)
		{
			return GetChildValue(childKey.ToString());
		}

		public void SetChildValue(double childKey, T value)
		{
			SetChildValue(childKey.ToString(), value);
		}

		public T GetChildValue(string childKey)
		{
			return GetValue(Key + Global.KeyDelimiter + childKey);
		}

		public void SetChildValue(string childKey, T value)
		{
			SetValue(Key + Global.KeyDelimiter + childKey, value);
		}

		// This caching solution for child objects helps avoiding the use of MemberwiseClone but 
		// will not work correctly unless the cache is reset every time a property is modified.
		//public virtual StoreBase<T, TStore> this[string index]
		//{
		//    get
		//    {
		//        if (IndexedStores == null)
		//            IndexedStores = new Dictionary<string, T1>();
		//
		//        if (!IndexedStores.ContainsKey(index))
		//            IndexedStores.Add(index, (StoreType)this.MemberwiseClone());
		//
		//        return IndexedStores[index];
		//    }
		//}

		public void RestoreConnectionAndSaveChanges()
		{
			SaveChanges(true);
		}

		/// <summary>
		/// Saves the changes applied to the internal buffer after the last disconnection or
		/// after a property has been changed.
		/// </summary>
		/// <param name="restoreConnection">When true the connection is restored before saving.
		/// Note that the connection is always restored even if an error occurs
		/// during the operation.</param>
		public void SaveChanges()
		{
			SaveChanges(false);
		}

		/// <summary>
		/// Saves the changes applied to the internal buffer after the last disconnection or
		/// after a property has been changed.
		/// </summary>
		/// <param name="restoreConnection">When true the connection is restored before saving.
		/// Note that the connection is always restored even if an error occurs
		/// during the operation.</param>
		public void SaveChanges(bool restoreConnection)
		{
			bool wasDisconnected = !_isConnected;

			if (restoreConnection)
				_isConnected = true;

			if (wasDisconnected && _valueBuffer == null)
				Clear();
			else
				SetValue(Key, wasDisconnected ? (T)_valueBuffer : GetValue(Key));
		}

		/// <summary>
		/// When disconnected all read/write operations are carried out directly in memory until
		/// the connection is restored. This helps minimize the overhead of accessing the specified
		/// storage for each operation and can also be used to avoid redirections when using the
		/// query string as target type.
		/// </summary>
		/// 
		public void Disconnect()
		{
			_valueBuffer = (T)GetValue(Key);
			_isConnected = false;
		}

		public bool IsConnected
		{
			get { return _isConnected; }
		}


		/// <summary>
		/// Performs a shallow copy of the current instance.
		/// </summary>
		/// <returns></returns>
		object ICloneable.Clone()
		{
			return this.MemberwiseClone();
		}

		/// <summary>
		/// Performs a shallow copy of the current instance.
		/// </summary>
		/// <returns></returns>
		public virtual TStore ShallowClone()
		{
			return (TStore)this.MemberwiseClone();
		}

		/// <summary>
		/// Gets the compression level achieved when compression is applied on the serialized value.
		/// </summary>
		/// <returns>The compression level as a percentage</returns>
		public int GetCompressionLevel()
		{
			string jsonValue = Serializer.Serialize<T>(this.Value, JsonEncoding.None, ThrowExceptionOnDeserializationOrDecryptionError);
			string compressedJsonValue = Compressor.CompressToBase64(jsonValue);
			return 100 * (compressedJsonValue.Length / jsonValue.Length);
		}

		/// <summary>
		/// Returns the length of the serialized value.
		/// </summary>
		/// <returns></returns>
		protected virtual long GetSize()
		{
			object value = this.PlainValue;
			return value == null ? 0 : value.ToString().Length;
		}

		public override string ToString()
		{
			return Serializer.Serialize<T>(Value, ThrowExceptionOnDeserializationOrDecryptionError);
		}

		public virtual string ToString(JsonEncoding encoder)
		{
			return Serializer.Serialize<T>(Value, encoder, ThrowExceptionOnDeserializationOrDecryptionError);
		}
	}
}
